# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2024 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#


import os
import re
import shutil
from enum import Enum
from html import escape
from pathlib import Path
from urllib.parse import urlparse, unquote

from gi.repository import GLib

from app.commons import run_idle, run_task, run_with_delay, log
from app.connections import upload_data, DownloadType, download_data, remove_picons
from app.settings import SettingsType, Settings, SEP, IS_DARWIN
from app.tools.picons import (PiconsParser, parse_providers, Provider, convert_to, download_picon, PiconsCzDownloader,
                              PiconsError, PiconFormat)
from app.tools.satellites import SatellitesParser, SatelliteSource
from .dialogs import show_dialog, DialogType, translate, get_builder, get_chooser_dialog
from .main_helper import (scroll_to, on_popup_menu, get_base_model, set_picon, get_picon_pixbuf, get_picon_dialog,
                          get_picon_file_name, get_pixbuf_from_data, get_pixbuf_at_scale, get_pos_num)
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TV_ICON, Column, KeyboardKey, Page, ViewTarget


class PiconManager(Gtk.Box):
    class DownloadSource(Enum):
        LYNG_SAT = "lyngsat"
        PICON_CZ = "piconcz"

    def __init__(self, app, settings, picon_ids, sat_positions, **kwargs):
        super().__init__(**kwargs)

        self._app = app
        self._app.connect("data-open", self.on_open)
        self._app.connect("data-receive", self.on_download)
        self._app.connect("data-send", self.on_send)
        self._app.connect("page-changed", self.update_picons_dest)
        self._app.connect("filter-toggled", self.on_app_filter_toggled)
        self._app.connect("profile-changed", self.on_profile_changed)
        self._app.connect("picon-assign", self.on_picon_assign)
        self._app.fav_view.connect("row-activated", self.on_fav_changed)
        self._picon_ids = picon_ids
        self._sat_positions = sat_positions
        self._BASE_URL = "www.lyngsat.com/packages/"
        self._PATTERN = re.compile(r"^https://www\.lyngsat\.com/[\w-]+\.html$")
        self._POS_PATTERN = re.compile(r"^\d+\.\d+[EW]?$")
        self._current_process = None
        self._terminate = False
        self._is_downloading = False
        self._filter_binding = None
        self._services = None
        self._current_picon_info = None
        self._filter_cache = {}
        # Downloader
        self._sats = None
        self._sat_names = None
        self._download_src = self.DownloadSource.PICON_CZ
        self._picon_cz_downloader = None

        handlers = {"on_tool_switched": self.on_tool_switched,
                    "on_add": self.on_add,
                    "on_extract": self.on_extract,
                    "on_receive": self.on_receive,
                    "on_cancel": self.on_cancel,
                    "on_remove": self.on_remove,
                    "on_selected_toggled": self.on_selected_toggled,
                    "on_url_changed": self.on_url_changed,
                    "on_picons_filter_changed": self.on_picons_filter_changed,
                    "on_position_edited": self.on_position_edited,
                    "on_convert": self.on_convert,
                    "on_picons_view_drag_data_get": self.on_picons_view_drag_data_get,
                    "on_picons_view_drag_drop": self.on_picons_view_drag_drop,
                    "on_picons_view_drag_data_received": self.on_picons_view_drag_data_received,
                    "on_picons_view_drag_end": self.on_picons_view_drag_end,
                    "on_picon_info_image_drag_data_received": self.on_picon_info_image_drag_data_received,
                    "on_selective_send": self.on_selective_send,
                    "on_selective_download": self.on_selective_download,
                    "on_selective_remove": self.on_selective_remove,
                    "on_local_remove": self.on_local_remove,
                    "on_download_source_changed": self.on_download_source_changed,
                    "on_satellites_view_realize": self.on_satellites_view_realize,
                    "on_satellite_filter_toggled": self.on_satellite_filter_toggled,
                    "on_providers_view_query_tooltip": self.on_providers_view_query_tooltip,
                    "on_satellite_selection": self.on_satellite_selection,
                    "on_select_all": self.on_select_all,
                    "on_unselect_all": self.on_unselect_all,
                    "on_filter_toggled": self.on_filter_toggled,
                    "on_fiter_srcs_toggled": self.on_fiter_srcs_toggled,
                    "on_picon_activated": self.on_picon_activated,
                    "on_view_query_tooltip": self.on_view_query_tooltip,
                    "on_tree_view_key_press": self.on_tree_view_key_press,
                    "on_popup_menu": on_popup_menu}

        builder = get_builder(UI_RESOURCES_PATH + "picons.glade", handlers)

        self._app_window = app.get_active_window()
        self._stack = builder.get_object("stack")
        self._picons_src_view = builder.get_object("picons_src_view")
        self._picons_dest_view = builder.get_object("picons_dest_view")
        self._providers_view = builder.get_object("providers_view")
        self._satellites_view = builder.get_object("satellites_view")
        self._picons_src_filter_model = builder.get_object("picons_src_filter_model")
        self._picons_src_filter_model.set_visible_func(self.picons_src_filter_function)
        self._picons_dst_filter_model = builder.get_object("picons_dst_filter_model")
        self._picons_dst_filter_model.set_visible_func(self.picons_dst_filter_function)
        self._src_filter_button = builder.get_object("src_filter_button")
        self._dst_filter_button = builder.get_object("dst_filter_button")
        self._picons_filter_entry = builder.get_object("picons_filter_entry")
        self._current_path_label = builder.get_object("current_path_label")
        self._download_source_button = builder.get_object("download_source_button")
        self._receive_button = builder.get_object("receive_button")
        self._convert_button = builder.get_object("convert_button")
        self._enigma2_path_button = builder.get_object("enigma2_path_button")
        self._save_to_button = builder.get_object("save_to_button")
        self._send_button = builder.get_object("send_button")
        self._download_button = builder.get_object("download_button")
        self._remove_button = builder.get_object("remove_button")
        self._cancel_button = builder.get_object("cancel_button")
        self._enigma2_radio_button = builder.get_object("enigma2_radio_button")
        self._neutrino_mp_radio_button = builder.get_object("neutrino_mp_radio_button")
        self._resize_no_radio_button = builder.get_object("resize_no_radio_button")
        self._resize_220_132_radio_button = builder.get_object("resize_220_132_radio_button")
        self._resize_100_60_radio_button = builder.get_object("resize_100_60_radio_button")
        self._satellite_label = builder.get_object("satellite_label")
        self._src_link_button = builder.get_object("src_link_button")
        self._satellite_filter_switch = builder.get_object("satellite_filter_switch")
        self._bouquet_filter_switch = builder.get_object("bouquet_filter_switch")
        self._providers_header_box = builder.get_object("providers_header_box")
        self._header_download_box = builder.get_object("header_download_box")
        self._converter_sc_button = builder.get_object("converter_sc_button")
        self._converter_nt_button = builder.get_object("converter_nt_button")
        self._converter_bq_button = builder.get_object("converter_bq_button")
        # Info.
        self._dst_count_label = builder.get_object("dst_count_label")
        self._info_check_button = builder.get_object("info_check_button")
        self._picon_info_image = builder.get_object("picon_info_image")
        self._picon_info_label = builder.get_object("picon_info_label")
        # Filter.
        self._filter_bar = builder.get_object("filter_bar")
        self._auto_filter_switch = builder.get_object("auto_filter_switch")
        self._filter_button = builder.get_object("filter_button")
        self._src_button = builder.get_object("src_button")
        # Header buttons. -> Used instead stack switcher.
        self._manager_button = builder.get_object("manager_button")
        self._downloader_button = builder.get_object("downloader_button")
        self._converter_button = builder.get_object("converter_button")
        # Init drag-and-drop
        self.init_drag_and_drop()
        # Rendering.
        column = builder.get_object("dest_picon_column")
        column.set_cell_data_func(builder.get_object("picons_dest_renderer"), self.picon_data_func)
        column = builder.get_object("src_picon_column")
        column.set_cell_data_func(builder.get_object("picons_src_renderer"), self.picon_data_func)
        column = builder.get_object("dest_title_column")
        column.set_cell_data_func(builder.get_object("title_dest_renderer"), self.title_data_func)
        # Settings
        self._settings = settings
        self._s_type = settings.setting_type
        self._current_path_label.set_text(self._settings.profile_picons_path)

        self.pack_start(builder.get_object("main_frame"), True, True, 0)
        self.show()

        if not len(self._picon_ids) and self._s_type is SettingsType.ENIGMA_2:
            message = translate("To automatically set the identifiers for picons,\n"
                                "first load the required services list into the main application window.")
            self.show_info_message(message, Gtk.MessageType.WARNING)
            self._satellite_label.show()

    def on_tool_switched(self, button):
        if not button.get_active():
            return True

        is_explorer = button is self._manager_button
        is_downloader = button is self._downloader_button
        is_converter = button is self._converter_button

        name = "explorer"
        if is_downloader:
            name = "downloader"
        elif is_converter:
            name = "converter"
            if not self._enigma2_path_button.get_filename():
                self._enigma2_path_button.set_filename(self._settings.profile_picons_path)

        self._stack.set_visible_child_name(name)

        self._convert_button.set_visible(is_converter)
        self._download_source_button.set_visible(is_downloader)
        self._filter_button.set_visible(is_explorer)
        if is_explorer:
            self.update_picons_data(self._picons_dest_view)

    def on_open(self, app, page):
        """ Opens picons from local path [in src view]. """
        if page is not Page.PICONS:
            return

        response = show_dialog(DialogType.CHOOSER, self._app.app_window, settings=self._settings, title="Open folder")
        if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
            return

        self._src_button.set_active(True)
        self.update_picons_data(self._picons_src_view, response)

    def update_picons_dest(self, app, page):
        if page is Page.PICONS:
            self._services = {s.picon_id: s for s in self._app.current_services.values() if s.picon_id}
            self.update_picons_data(self._picons_dest_view)

    def on_profile_changed(self, app, data):
        self._current_path_label.set_text(self._settings.profile_picons_path)
        self.update_picons_dest(app, self._app.page)
        self._enigma2_path_button.set_filename(self._settings.profile_picons_path)

    def on_picon_assign(self, app, target):
        if target is ViewTarget.SERVICES:
            model, paths = app.services_view.get_selection().get_selected_rows()
            ids = {model[p][Column.SRV_FAV_ID] for p in paths}
        else:
            model, paths = app.fav_view.get_selection().get_selected_rows()
            ids = {model[p][Column.FAV_ID] for p in paths}

        self._filter_button.set_active(True)
        self._dst_filter_button.set_active(True)
        self._picons_filter_entry.set_text(
            "|".join(s.service for f, s in self._app.current_services.items() if f in ids))

    def update_picons_data(self, view, path=None):
        if view is self._picons_dest_view:
            self.update_picon_info()

        gen = self.update_picons(path or self._settings.profile_picons_path, view)
        GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)

    def update_picons(self, path, view):
        p_model = view.get_model()
        model = get_base_model(p_model)
        factor = self._app.DEL_FACTOR * 2

        for index, itr in enumerate([row.iter for row in model]):
            model.remove(itr)
            if index % factor == 0:
                yield True

        self._dst_count_label.set_text("0")
        os.makedirs(os.path.dirname(path), exist_ok=True)
        for index, file in enumerate(os.listdir(path)):
            if self._terminate:
                return

            model.append((None, file, f"{path}{SEP}{file}"))
            if index % factor == 0:
                self._dst_count_label.set_text(str(len(model)))
                yield True

        self._dst_count_label.set_text(str(len(model)))
        yield True

    def picon_data_func(self, column, renderer, model, itr, data):
        renderer.set_property("pixbuf", get_pixbuf_at_scale(model.get_value(itr, 2), 72, 48, True))

    def title_data_func(self, column, renderer, model, itr, data):
        srv = self._services.get(model[itr][1], None)
        if srv:
            renderer.set_property("markup", self.get_picon_info_markup(srv))

    def get_picon_info_markup(self, srv):
        ext_info = "" if srv.service_type == "IPTV" else f" {srv.pos} {srv.freq}"
        return (f'{escape(srv.picon_id)}\n\n'
                f'<span size="small" weight="bold">{translate("Service")}: {escape(srv.service)}</span>\n'
                f'<span size="small" style="italic">{srv.service_type}{ext_info}</span>')

    def update_picons_from_file(self, view, uri):
        """ Adds picons in the view on dragging from file system. """
        path = Path(urlparse(unquote(uri)).path.strip())
        f_path = str(path.resolve())
        if not f_path:
            return

        model = get_base_model(view.get_model())

        if path.is_file():
            p = get_pixbuf_at_scale(f_path, 72, 48, True)
            if p:
                model.append((p, path.name, f_path))
        elif path.is_dir():
            self.update_picons_data(view, f_path)

    # ***************** Drag-and-drop ********************* #

    def init_drag_and_drop(self):
        self._picons_src_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
        self._picons_src_view.drag_source_add_uri_targets()

        self._picons_dest_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
        self._picons_dest_view.drag_source_add_uri_targets()

        self._picons_src_view.enable_model_drag_dest([], Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE)
        self._picons_src_view.drag_dest_add_text_targets()

        self._picons_dest_view.enable_model_drag_dest([], Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE)
        self._picons_dest_view.drag_dest_add_text_targets()

        self._picon_info_image.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
        self._picon_info_image.drag_dest_add_uri_targets()

    def on_picons_view_drag_data_get(self, view, drag_context, data, info, time):
        model, path = view.get_selection().get_selected_rows()
        if path:
            dest_uri = Path(self._settings.profile_picons_path).as_uri()
            if IS_DARWIN:
                data.set_uris([f"{Path(model[path][-1]).as_uri()}{self._app.DRAG_SEP}{dest_uri}"])
            else:
                data.set_uris([Path(model[path][-1]).as_uri(), dest_uri])

    def on_picons_view_drag_drop(self, view, drag_context, x, y, time):
        view.stop_emission_by_name("drag_drop")
        targets = drag_context.list_targets()
        view.drag_get_data(drag_context, targets[-1] if targets else Gdk.atom_intern("text/plain", False), time)

    def on_picons_view_drag_data_received(self, view, drag_context, x, y, data, info, time):
        view.stop_emission_by_name("drag_data_received")
        txt = data.get_text()
        if not txt:
            return

        if txt.startswith("file://"):
            self.update_picons_from_file(view, txt)
            return

        itr_str, sep, src = txt.partition(self._app.DRAG_SEP)
        if src == self._app.BQ_MODEL:
            return

        path, pos = view.get_dest_row_at_pos(x, y) or (None, None)
        if not path:
            return

        model = view.get_model()
        if src == self._app.FAV_MODEL:
            target_view = self._app.fav_view
            c_id = Column.FAV_ID
        else:
            target_view = self._app.services_view
            c_id = Column.SRV_FAV_ID

        t_mod = target_view.get_model()
        dest_path = self._settings.profile_picons_path
        self.update_picons_dest_view(self._app.on_assign_picon_file(target_view, model[path][-1], dest_path))
        self.show_assign_info([t_mod.get_value(t_mod.get_iter_from_string(itr), c_id) for itr in itr_str.split(",")])

    @run_idle
    def update_picons_dest_view(self, picons):
        """ Update destination view on adding/changing picons. """
        if picons:
            dest_model = get_base_model(self._picons_dest_view.get_model())
            paths = {r[1]: r.iter for r in dest_model}

            for p_path in picons:
                p = get_pixbuf_at_scale(p_path, 72, 48, True)
                if p:
                    p_name = Path(p_path).name
                    itr = paths.get(p_name, None)
                    if itr:
                        dest_model.set_value(itr, 0, p)
                    else:
                        itr = dest_model.append((p, p_name, p_path))
                    scroll_to(dest_model.get_path(itr), self._picons_dest_view)

            self._dst_count_label.set_text(str(len(dest_model)))

    @run_idle
    def show_assign_info(self, fav_ids):
        self._app.change_action_state("on_logs_show", GLib.Variant.new_boolean(True))
        for i in fav_ids:
            srv = self._app.current_services.get(i, None)
            if srv:
                info = self._app.get_hint_for_srv_list(srv)
                log(f"Picon assignment for the service:\n{info}\n{' * ' * 30}\n")

    def on_picons_view_drag_end(self, view, drag_context):
        self.update_picons_dest_view(self._app.picons_buffer)

    def on_picon_info_image_drag_data_received(self, img, drag_context, x, y, data, info, time):
        if not self._current_picon_info:
            self.show_info_message("No selected item!", Gtk.MessageType.ERROR)
            return

        uris = data.get_uris()
        if len(uris) == 2:
            name, fav_id = self._current_picon_info
            src = urlparse(unquote(uris[0])).path
            dst = f"{urlparse(unquote(uris[1])).path}{SEP}{name}"
            if src != dst:
                shutil.copy(src, dst)
                for row in get_base_model(self._picons_dest_view.get_model()):
                    if name == row[1]:
                        row[0] = get_pixbuf_at_scale(row[-1], 72, 48, True)
                        img.set_from_pixbuf(get_pixbuf_at_scale(row[-1], 100, 60, True))

                gen = self.update_picon_in_lists(dst, fav_id)
                GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)

    def get_path_from_uris(self, data):
        uris = data.get_uris()
        if len(uris) == 2:
            return Path(urlparse(unquote(uris[0])).path).resolve()

    def update_picon_in_lists(self, dst, fav_id):
        picon = get_picon_pixbuf(dst)
        p_pos = Column.SRV_PICON
        yield set_picon(fav_id, get_base_model(self._app.services_view.get_model()), picon, Column.SRV_FAV_ID, p_pos)
        yield set_picon(fav_id, get_base_model(self._app.fav_view.get_model()), picon, Column.FAV_ID, p_pos)

    # ************************ Add/Extract ******************************** #

    def on_add(self, item):
        """ Adds (copies) picons from an external folder to the profile picons folder. """
        dialog = get_picon_dialog(self._app_window, translate("Add picons"), translate("Add"))
        if dialog.run() in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
            return

        self.copy_picons_file(dialog.get_filenames())

    def on_extract(self, item):
        """ Extracts picons from an archives to the profile picons folder. """
        file_filter = None
        if IS_DARWIN:
            file_filter = Gtk.FileFilter()
            file_filter.set_name("*.zip, *.gz")
            file_filter.add_mime_type("application/zip")
            file_filter.add_mime_type("application/gzip")

        response = get_chooser_dialog(self._app_window, self._settings, "*.zip, *.gz files",
                                      ("*.zip", "*.gz"), "Extract picons", file_filter)
        if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
            return

        arch_path = self._app.get_archive_path(response)
        if arch_path:
            self.copy_picons_file(Path(arch_path.name).glob("*.png"), arch_path.cleanup)

    def copy_picons_file(self, files, callback=None):
        """ Copies files to the profile picons folder. """
        picon_path = self._settings.profile_picons_path
        os.makedirs(os.path.dirname(picon_path), exist_ok=True)

        try:
            picons = [shutil.copy(p, picon_path) for p in files]
        except shutil.SameFileError as e:
            log(e)
            self.show_info_message(str(e), Gtk.MessageType.ERROR)
        else:
            self.update_picons_dest_view(picons)
            self._app.update_picons()
        finally:
            if callback:
                callback()

    # ******************** Download/Upload/Remove ************************* #

    def on_selective_send(self, view):
        path = self.get_selected_path(view)
        if path:
            self.on_picons_send(files_filter={path.name}, path=path.parent)

    def on_selective_download(self, view):
        path = self.get_selected_path(view)
        if path:
            self.on_picons_download(files_filter={path.name})

    def on_selective_remove(self, view):
        path = self.get_selected_path(view)
        if path:
            self.on_remove(files_filter={path.name})

    def on_local_remove(self, view):
        model, paths = view.get_selection().get_selected_rows()
        if paths and show_dialog(DialogType.QUESTION, self._app_window) == Gtk.ResponseType.OK:
            base_model = get_base_model(model)
            filter_model = model.get_model()
            to_del = []

            for p in paths:
                itr = model.get_iter(p)
                p_path = Path(model.get_value(itr, 2)).resolve()
                if p_path.is_file():
                    p_path.unlink()
                    to_del.append(filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr)))

            list(map(base_model.remove, to_del))
            self._app.update_picons()

        if view is self._picons_dest_view:
            self._dst_count_label.set_text(str(len(model)))

    def on_send(self, app, page):
        if page is Page.PICONS:
            view = self._picons_src_view if self._picons_src_view.is_focus() else self._picons_dest_view
            model, paths = view.get_selection().get_selected_rows()
            if paths:
                self.on_picons_send(files_filter={Path(model[p][-1]).resolve().name for p in paths})
            else:
                self._app.show_error_message("No selected item!")

    def on_picons_send(self, item=None, files_filter=None, path=None):
        dest_path = path or self._settings.profile_picons_path
        settings = Settings(self._settings.settings)
        settings.profile_picons_path = f"{dest_path}{SEP}"
        settings.current_profile = self._settings.current_profile
        self.show_info_message(translate("Please, wait..."), Gtk.MessageType.INFO)
        self.run_func(lambda: upload_data(settings=settings,
                                          download_type=DownloadType.PICONS,
                                          done_callback=lambda: self.show_info_message(translate("Done!"),
                                                                                       Gtk.MessageType.INFO),
                                          files_filter=files_filter))

    def on_download(self, app, page):
        if page is Page.PICONS:
            self._app.picons.clear()
            self.on_picons_download()

    def on_picons_download(self, item=None, files_filter=None, path=None):
        path = path or self._settings.profile_picons_path
        settings = Settings(self._settings.settings)
        settings.profile_picons_path = path + SEP
        settings.current_profile = self._settings.current_profile
        self.run_func(lambda: download_data(settings=settings,
                                            download_type=DownloadType.PICONS,
                                            files_filter=files_filter), True)

    def on_remove(self, item=None, files_filter=None):
        if show_dialog(DialogType.QUESTION, self._app_window) == Gtk.ResponseType.CANCEL:
            return

        self.run_func(lambda: remove_picons(settings=self._settings,
                                            done_callback=lambda: self.show_info_message(translate("Done!"),
                                                                                         Gtk.MessageType.INFO),
                                            files_filter=files_filter))

    def get_selected_path(self, view):
        model, paths = view.get_selection().get_selected_rows()
        if paths:
            return Path(model[paths.pop()][-1]).resolve()

    # ******************** Downloader ************************* #

    def on_download_source_changed(self, button):
        self._download_src = self.DownloadSource(button.get_active_id())
        self.set_providers_header()
        self._providers_header_box.set_sensitive(self._download_src is self.DownloadSource.PICON_CZ)
        GLib.idle_add(self._providers_view.get_model().clear)
        self.init_satellites(self._satellites_view)

    def on_satellites_view_realize(self, view):
        self.set_providers_header()
        self.get_satellites(view)

    def on_satellite_filter_toggled(self, button, state):
        self.init_satellites(self._satellites_view)

    def on_providers_view_query_tooltip(self, view, x, y, keyboard_mode, tooltip):
        if self._download_src is self.DownloadSource.LYNG_SAT:
            return False

        dest = view.get_dest_row_at_pos(x, y)
        if not dest:
            return False

        path, pos = dest
        model = view.get_model()
        itr = model.get_iter(path)
        logo_url = model.get_value(itr, 5)
        if logo_url:
            pix_data = self._picon_cz_downloader.get_logo_data(logo_url)
            if pix_data:
                pix = get_pixbuf_from_data(pix_data)
                model.set_value(itr, 0, pix if pix else TV_ICON)
                size = self._settings.tooltip_logo_size
                tooltip.set_icon(get_pixbuf_from_data(pix_data, size, size))
            else:
                self.update_logo_data(itr, model, logo_url)
        tooltip.set_text(model.get_value(itr, 1))
        view.set_tooltip_row(tooltip, path)
        return True

    @run_task
    def update_logo_data(self, itr, model, url):
        pix_data = self._picon_cz_downloader.get_provider_logo(url)
        if pix_data:
            pix = get_pixbuf_from_data(pix_data)
            GLib.idle_add(model.set_value, itr, 0, pix if pix else TV_ICON)

    @run_idle
    def set_providers_header(self):
        if self._download_src is self.DownloadSource.PICON_CZ:
            link = "https://picon.cz"
            tooltip = f"{link} (by Chocholoušek)"
        elif self._download_src is self.DownloadSource.LYNG_SAT:
            link = "https://www.lyngsat.com"
            tooltip = f"{translate('Providers')} [{link}]"
        else:
            link = ""
            tooltip = ""

        self._src_link_button.set_uri(link)
        self._src_link_button.set_label(link)
        self._src_link_button.set_tooltip_text(tooltip)

    @run_task
    def get_satellites(self, view):
        self._sats = SatellitesParser().get_satellites_list(SatelliteSource.LYNGSAT)
        if not self._sats:
            self.show_info_message("Getting satellites list error!", Gtk.MessageType.ERROR)

        self._sat_names = {s[1]: s[0] for s in self._sats}  # position -> satellite name
        self._picon_cz_downloader = PiconsCzDownloader(self._picon_ids)
        self.init_satellites(view)

    @run_task
    def init_satellites(self, view):
        sats = self._sats
        if self._download_src is self.DownloadSource.PICON_CZ:
            if not self._picon_cz_downloader:
                return
            try:
                self._picon_cz_downloader.init()
            except PiconsError as e:
                self.show_info_message(str(e), Gtk.MessageType.ERROR)
            else:
                providers = self._picon_cz_downloader.providers
                sats = ((self._sat_names.get(p, p), p, None, p, False) for p in providers)
        gen = self.append_satellites(view.get_model(), sats)
        GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)

    def append_satellites(self, model, sats):
        is_filter = self._satellite_filter_switch.get_active()
        if model:
            model.clear()

        try:
            for sat in sorted(sats, key=lambda s: get_pos_num(s[1]), reverse=True):
                pos = sat[1]
                name = f"{sat[0]} ({pos})"
                if is_filter and pos not in self._sat_positions:
                    continue
                if not model:
                    return
                yield model.append((name, sat[3], pos))
        finally:
            self._satellite_label.show()

    def on_satellite_selection(self, view, path, column):
        model = self._providers_view.get_model()
        model.clear()
        self._satellite_label.set_visible(False)
        self.get_providers(view.get_model()[path][1], model)

    @run_task
    def get_providers(self, url, model):
        if self._download_src is self.DownloadSource.LYNG_SAT:
            providers = parse_providers(url)
        elif self._download_src is self.DownloadSource.PICON_CZ:
            providers = self._picon_cz_downloader.get_sat_providers(url)
        else:
            return

        self.append_providers(providers or [], model)

    @run_idle
    def append_providers(self, providers, model):
        if self._download_src is self.DownloadSource.LYNG_SAT:
            for p in providers:
                model.append(p._replace(logo=get_pixbuf_from_data(p.logo) if p.logo else TV_ICON))
        elif self._download_src is self.DownloadSource.PICON_CZ:
            for p in providers:
                logo_data = self._picon_cz_downloader.get_logo_data(p.ssid)
                model.append(p._replace(logo=get_pixbuf_from_data(logo_data) if logo_data else TV_ICON))

        self.update_receive_button_state()
        GLib.idle_add(self._satellite_label.set_visible, True)

    def on_receive(self, item):
        if self._is_downloading:
            self._app.show_error_message("The task is already running!")
            return

        providers = self.get_selected_providers()

        if self._download_src is self.DownloadSource.PICON_CZ and len(providers) > 1:
            self._app.show_error_message("Please, select only one item!")
            return

        self._cancel_button.show()
        self.start_download(providers)

    @run_task
    def start_download(self, providers):
        self._is_downloading = True
        self._app.change_action_state("on_logs_show", GLib.Variant.new_boolean(True))

        for prv in providers:
            if self._download_src is self.DownloadSource.LYNG_SAT and not self._POS_PATTERN.match(prv[2]):
                self.show_info_message(
                    translate("Specify the correct position value for the provider!"), Gtk.MessageType.ERROR)
                scroll_to(prv.path, self._providers_view)
                return

        try:
            picons_path = self._current_path_label.get_text()
            os.makedirs(os.path.dirname(picons_path), exist_ok=True)
            self.show_info_message(translate("Please, wait..."), Gtk.MessageType.INFO)
            providers = (Provider(*p) for p in providers)

            if self._download_src is self.DownloadSource.LYNG_SAT:
                self.get_picons_for_lyngsat(picons_path, providers)
            elif self._download_src is self.DownloadSource.PICON_CZ:
                self.get_picons_for_picon_cz(picons_path, providers)

            if not self._is_downloading:
                return

            if not self._resize_no_radio_button.get_active():
                self.resize(picons_path)
        finally:
            self._app.update_picons()
            GLib.idle_add(self._cancel_button.hide)
            self._is_downloading = False

    def get_picons_for_lyngsat(self, path, providers):
        import concurrent.futures

        with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
            picons = []
            # Getting links to picons.
            futures = {executor.submit(self.process_provider, p, path): p for p in providers}
            for future in concurrent.futures.as_completed(futures):
                if not self._is_downloading:
                    executor.shutdown()
                    return

                pic = future.result()
                if pic:
                    picons.extend(pic)
            # Getting picon images.
            futures = {executor.submit(download_picon, *pic): pic for pic in picons}
            done, not_done = concurrent.futures.wait(futures, timeout=0)
            while self._is_downloading and not_done:
                done, not_done = concurrent.futures.wait(not_done, timeout=5)

            for future in not_done:
                future.cancel()
            concurrent.futures.wait(not_done)
            self.show_info_message(translate("Done!"), Gtk.MessageType.INFO)

    def get_picons_for_picon_cz(self, path, providers):
        p_ids = None
        if self._bouquet_filter_switch.get_active():
            p_ids = self.get_bouquet_picon_ids()
            if not p_ids:
                return

        try:
            # We download it sequentially.
            [self._picon_cz_downloader.download(p, path, p_ids) for p in providers]
        except PiconsError as e:
            log(f"Error: {str(e)}\n")
            self.show_info_message(str(e), Gtk.MessageType.ERROR)
        else:
            self.show_info_message(translate("Done!"), Gtk.MessageType.INFO)

    def get_bouquet_picon_ids(self):
        """ Returns picon ids for selected bouquet or None. """
        bq_selected = self._app.check_bouquet_selection()
        if not bq_selected:
            return

        model, paths = self._app.bouquets_view.get_selection().get_selected_rows()
        if len(paths) > 1:
            self._app.show_error_message("Please, select only one bouquet!")
            return

        fav_bouquet = self._app.current_bouquets[bq_selected]
        services = self._app.current_services

        ids = set()
        for s in (services.get(fav_id, None) for fav_id in fav_bouquet):
            if s:
                ids.add(s.picon_id)
                ids.add(get_picon_file_name(s.service))
        return ids

    def process_provider(self, prv, picons_path):
        log(f"Getting links to picons for: {prv.name}.\n")
        return PiconsParser.parse(prv, picons_path, self._picon_ids, self.get_picons_format())

    @run_task
    def resize(self, path):
        self.show_info_message(translate("Resizing..."), Gtk.MessageType.INFO)

        try:
            from pathlib import Path
            from PIL import Image
        except ImportError as e:
            self.show_info_message(f"{translate('Conversion error.')} {e}", Gtk.MessageType.ERROR)
        else:
            res = (220, 132) if self._resize_220_132_radio_button.get_active() else (100, 60)

            for img_file in Path(path).glob("*.png"):
                img = Image.open(img_file)
                img = img.resize(res, Image.ANTIALIAS)
                img.save(img_file, "PNG", optimize=True)

            self.show_info_message(translate("Done!"), Gtk.MessageType.INFO)

    def on_cancel(self, item=None):
        if self._is_downloading and show_dialog(DialogType.QUESTION, self._app_window) == Gtk.ResponseType.CANCEL:
            return True

        self.terminate_task()

    @run_task
    def terminate_task(self):
        self._terminate = True
        self._is_downloading = False
        self.show_info_message(translate("The task is canceled!"), Gtk.MessageType.WARNING)

    @run_task
    def run_func(self, func, update=False):
        try:
            self._app.change_action_state("on_logs_show", GLib.Variant.new_boolean(True))
            GLib.idle_add(self._header_download_box.set_sensitive, False)
            func()
        except OSError as e:
            self.show_info_message(str(e), Gtk.MessageType.ERROR)
        finally:
            GLib.idle_add(self._header_download_box.set_sensitive, True)
            if update:
                self.update_picons_data(self._picons_dest_view)

    def show_info_message(self, text, message_type):
        self._app.show_info_message(text, message_type)

    @run_idle
    def on_selected_toggled(self, toggle, path):
        model = self._providers_view.get_model()
        model.set_value(model.get_iter(path), 7, not toggle.get_active())
        self.update_receive_button_state()

    def on_select_all(self, view):
        self.update_selection(view, True)

    def on_unselect_all(self, view):
        self.update_selection(view, False)

    def update_selection(self, view, select):
        view.get_model().foreach(lambda mod, path, itr: mod.set_value(itr, 7, select))
        self.update_receive_button_state()

    # *********************** Filter **************************** #

    def on_app_filter_toggled(self, app, value):
        if app.page is Page.PICONS:
            self._filter_button.set_active(not self._filter_button.get_active())

    def on_fav_changed(self, view, path, column):
        if self._app.page is Page.PICONS and self._auto_filter_switch.get_active():
            model = view.get_model()
            self._picons_filter_entry.set_text(model.get_value(model.get_iter(path), Column.FAV_SERVICE))

    def on_filter_toggled(self, button):
        active = self._filter_button.get_active()
        if not active:
            self._picons_filter_entry.set_text("")

    def on_fiter_srcs_toggled(self, filter_model):
        """ Activates re-filtering for model when filter check-button has toggled. """
        GLib.idle_add(filter_model.refilter, priority=GLib.PRIORITY_LOW)

    @run_with_delay(0.5)
    def on_picons_filter_changed(self, entry):
        self._filter_cache.clear()
        txt = entry.get_text().upper().split("|")
        for s in self._app.current_services.values():
            self._filter_cache[s.picon_id] = any(t in s.service.upper() or t in str(s.picon_id) for t in txt)

        GLib.idle_add(self._picons_src_filter_model.refilter, priority=GLib.PRIORITY_LOW)
        GLib.idle_add(self._picons_dst_filter_model.refilter, priority=GLib.PRIORITY_LOW)

    def picons_src_filter_function(self, model, itr, data):
        return self.filter_function(itr, model, self._src_filter_button.get_active())

    def picons_dst_filter_function(self, model, itr, data):
        return self.filter_function(itr, model, self._dst_filter_button.get_active())

    def filter_function(self, itr, model, active):
        """ Main filtering function. """
        if any((not active, model is None, model == "None")):
            return True

        t = model.get_value(itr, 1)
        if not t:
            return True

        txt = self._picons_filter_entry.get_text().upper()
        return txt in t.upper() or self._filter_cache.get(t, False)

    def on_picon_activated(self, view):
        if self._info_check_button.get_active():
            model, path = view.get_selection().get_selected_rows()
            if not path:
                return

            row = model[path][:]
            name, path = row[1], row[-1]
            srv = self._services.get(row[1], None)
            self.update_picon_info(name, path, srv)

    def update_picon_info(self, name=None, path=None, srv=None):
        self._picon_info_image.set_from_pixbuf(get_pixbuf_at_scale(path, 100, 60, True) if path else None)
        self._picon_info_label.set_text(self.get_service_info(srv))
        self._current_picon_info = (name, srv.fav_id) if srv else None

    def get_service_info(self, srv):
        """ Returns short info about the service. """
        if not srv:
            return ""

        if srv.service_type == "IPTV":
            return self._app.get_hint_for_srv_list(srv)

        header, ref = self._app.get_hint_header_info(srv)
        return "{}  {}: {}\n{}: {}  {}: {}\n{}".format(header.rstrip(), translate("Package"), srv.package,
                                                       translate("System"), srv.system, translate("Freq"), srv.freq,
                                                       ref)

    def on_view_query_tooltip(self, view, x, y, keyboard_mode, tooltip):
        dest = view.get_dest_row_at_pos(x, y)
        if not dest:
            return False

        path, pos = dest
        model = view.get_model()
        row = model[path][:]
        tooltip.set_icon(get_picon_pixbuf(row[-1], size=self._settings.tooltip_logo_size))
        tooltip.set_text(row[1])
        view.set_tooltip_row(tooltip, path)

        return True

    def on_tree_view_key_press(self, view, event):
        key_code = event.hardware_keycode
        if not KeyboardKey.value_exist(key_code):
            return

        key = KeyboardKey(key_code)
        if key is KeyboardKey.DELETE:
            self.on_local_remove(view)

    def on_url_changed(self, entry):
        suit = self._PATTERN.search(entry.get_text())
        entry.set_name("GtkEntry" if suit else "digit-entry")
        self._download_source_button.set_sensitive(suit if suit else False)

    def on_position_edited(self, render, path, value):
        model = self._providers_view.get_model()
        model.set_value(model.get_iter(path), 2, value)

    @run_idle
    def on_convert(self, item):
        if show_dialog(DialogType.QUESTION, self._app_window) == Gtk.ResponseType.CANCEL:
            return

        picons_path = self._enigma2_path_button.get_filename()
        save_path = self._save_to_button.get_filename()
        if not picons_path or not save_path:
            self._app.show_error_message("Select paths!")
            return

        self._app.change_action_state("on_logs_show", GLib.Variant.new_boolean(True))
        ids = None
        p_format = PiconFormat.NEUTRINO if self._converter_nt_button.get_active() else PiconFormat.OSCAM

        if p_format is PiconFormat.OSCAM:
            try:
                from PIL import Image
            except ImportError as e:
                self.show_info_message(f"{translate('Conversion error.')} {e}", Gtk.MessageType.ERROR)
                return

        if self._converter_bq_button.get_active():
            bq_selected = self._app.check_bouquet_selection()
            if not bq_selected:
                return

            services = self._app.current_services
            ids = {services.get(s).picon_id for s in self._app.current_bouquets.get(bq_selected) if s in services}

        convert_to(src_path=picons_path, dest_path=save_path, p_format=p_format, ids=ids, services=self._services,
                   done_callback=lambda: self.show_info_message(translate("Done!"), Gtk.MessageType.INFO))

    @run_idle
    def update_receive_button_state(self):
        try:
            self._receive_button.set_sensitive(len(self.get_selected_providers()) > 0)
        except TypeError:
            pass  # NOP

    def get_selected_providers(self):
        """ returns selected providers """
        return [r for r in self._providers_view.get_model() if r[7]]

    @run_idle
    def show_dialog(self, message, dialog_type):
        show_dialog(dialog_type, self._app_window, message)

    def get_picons_format(self):
        return SettingsType.NEUTRINO_MP if self._neutrino_mp_radio_button.get_active() else SettingsType.ENIGMA_2


if __name__ == "__main__":
    pass
